#!/usr/bin/env python2.7
#-*- coding=utf-8 -*-

import struct
import inspect

class DataTypes(object):
    def typelen(self, typestr, number = 1):
        if typestr in ('Bool', 'Char', 'Byte', 'uByte', 'CString', 'PString'):
            return number * 1
        if typestr in ('Int16', 'uInt16', 'WORD'):
            return number * 2
        if typestr in ('Int32', 'uInt32', 'DWORD', 'Float'):
            return number * 4
        if typestr in ('Int64', 'uInt64', 'QWORD', 'Double'):
            return number * 8
        if typestr == 'Point':
            import platform
            bits, exetype = platform.architecture()
            if bits == '32bit':
                return number * 4
            if bits == '64bit':
                return number * 8
            raise TypeError('Invalid platform information => [%s:%s]' % (bits, exetype))
        raise TypeError('Invalid data type [%s]' % typestr)

    Bool = '?'
    Char = 'c'
    Byte = 'b'
    uByte = 'B'
    Int16 = 'h'
    uInt16 = 'H'
    WORD = uInt16
    Int32 = 'i'
    uInt32 = 'I'
    DWORD = uInt32
    Int64 = 'q'
    uInt64 = 'Q'
    QWORD = uInt64
    Float = 'f'
    Double = 'd'
    CString = 's'
    PString = 'p'
    Point = 'P'

class CStruct(object):
    def __init__(self, data, endian = '<'):
        super(CStruct, self).__init__()
        # Get own class name
        self.classname = self.__class__.__name__
        # Checking type of data is file or string
        if isinstance(data, file):
            isfile = True
        elif isinstance(data, (str, unicode)):
            isfile = False
        else:
            raise TypeError('Input data must be a string or a file object!')
        # Resolving _fields_
        self.datalen = 0
        datatypes = DataTypes()
        for item in self._fields_:
            # Getting subitems in item
            name = item[0]
            ctype = item[1]
            try:
                number = item[2]
                if type(number) != int or number < 1:
                    raise ValueError('number must int and not less than 1!')
            except IndexError:
                number = 1
            if endian is None:
                endian = item[3]
            # Resolve Struct and Union
            if inspect.isclass(ctype):
                if issubclass(ctype, (CStruct, CUnion)):
                    dataclass = ctype(data, endian)
                    if not isfile:
                        data = dataclass.restdata
                    self.datalen += len(dataclass)
                    # Set the attribute
                    setattr(self, name, dataclass)
                    continue
                raise ValueError('Invalid class [%s]' % ctype)
            # Getting unpack string
            unpackstr = getattr(datatypes, ctype, None)
            if unpackstr is None:
                raise ValueError('Invalid type [%s]' % ctype)
            if number != 1:
                unpackstr = str(number) + unpackstr
            # Getting length of current type
            typelen = datatypes.typelen(ctype, number)
            self.datalen += typelen
            # Read file or data
            if isfile:
                currentdata = data.read(typelen)
            else:
                currentdata = data[:typelen]
                data = data[typelen:]
            if len(currentdata) < typelen:
                raise BufferError('Data buffer is not long enough!')
            # Unpack current data
            unpackdata = struct.unpack(endian + unpackstr, currentdata)
            if len(unpackdata) == 1:
                unpackdata = unpackdata[0]
            # Set the attribute
            setattr(self, name, unpackdata)
        # Rest of data
        self.restdata = data

    def __len__(self):
        return self.datalen

    def getnames(self):
        names = []
        for item in self._fields_:
            names.append(item[0])
        return names

    _fields_ = []

class BigendCStruct(CStruct):
    def __init__(self, data):
        super(BigendCStruct, self).__init__(data, '>')

class CUnion(object):
    def __init__(self, data, endian = '<'):
        super(CUnion, self).__init__()
        # Get own class name
        self.classname = self.__class__.__name__
        # Checking type of data is file or string
        if isinstance(data, file):
            isfile = True
            fpoint = data.tell()
        elif isinstance(data, (str, unicode)):
            isfile = False
        else:
            raise TypeError('Input data must be a string or a file object!')
        # Resolving _fields_
        self.datalen = 0
        datatypes = DataTypes()
        for item in self._fields_:
            # Getting subitems in item
            name = item[0]
            ctype = item[1]
            try:
                number = item[2]
                if type(number) != int or number < 1:
                    raise ValueError('number must int and not less than 1!')
            except IndexError:
                number = 1
            if endian is None:
                endian = item[3]
            # Resolve Struct and Union
            if inspect.isclass(ctype):
                if issubclass(ctype, (CStruct, CUnion)):
                    if isfile:
                        data.seek(fpoint, 0)
                    dataclass = ctype(data, endian)
                    self.datalen = max(len(dataclass), self.datalen)
                    setattr(self, name, dataclass)
                    continue
                raise ValueError('Invalid class [%s]' % ctype)
            # Getting unpack string
            unpackstr = getattr(datatypes, ctype, None)
            if unpackstr is None:
                raise ValueError('Invalid type [%s]' % ctype)
            if number != 1:
                unpackstr = str(number) + unpackstr
            # Getting length of current type
            typelen = datatypes.typelen(ctype, number)
            self.datalen = max(self.datalen, typelen)
            # Read file or data
            if isfile:
                data.seek(fpoint, 0)
                currentdata = data.read(typelen)
            else:
                currentdata = data[:typelen]
            if len(currentdata) < typelen:
                raise BufferError('Data buffer is not long enough!')
            # Unpack current data
            unpackdata = struct.unpack(endian + unpackstr, currentdata)
            setattr(self, name, unpackdata)
        if isfile:
            data.seek(fpoint + self.datalen, 0)
        else:
            data = data[self.datalen:]
        self.restdata = data

    def __len__(self):
        return self.datalen

    def getnames(self):
        names = []
        for item in self._fields_:
            names.append(item[0])
        return names

    _fields_ = []

class BigendCUnion(CUnion):
    def __init__(self, data):
        super(BigendCUnion, self).__init__(data, '>')
